{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If running in a new environment, such as Google Colab, run this first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# !git clone https://github.com/zach401/acnportal.git\n",
    "# !pip install acnportal/."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ACN-Sim Tutorial: Lesson 2\n",
    "## Implementing a Custom Algorithm\n",
    "### by Zachary Lee\n",
    "#### Last updated: 03/19/2019\n",
    "\n",
    "In this lesson we will learn how to develop a custom algorithm and run it using ACN-Sim. For this example we will be writing an Earliest Deadline First Algorithm. This algorithm is already available as part of the SortingAlgorithm in the algorithms package, so we will compare the results of our implementation with the included one."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom Algorithm\n",
    "\n",
    "All custom algorithms should inherit from the abstract class BaseAlgorithm. It is the responsibility of all derived classes to implement the schedule method. This method takes as an input a list of EVs which are currently connected to the system but have not yet finished charging. Its output is a dictionary which maps a station_id to a list of charging rates. Each charging rate is valid for one period measured relative to the current period.\n",
    "\n",
    "For Example: \n",
    " * schedule[‘abc’][0] is the charging rate for station ‘abc’ during the current period \n",
    " * schedule[‘abc’][1] is the charging rate for the next period \n",
    " * and so on. \n",
    " \n",
    "If an algorithm only produces charging rates for the current time period, the length of each list should be 1. If this is the case, make sure to also set the maximum resolve period to be 1 period so that the algorithm will be called each period. An alternative is to repeat the charging rate a number of times equal to the max recompute period.\n",
    "\n",
    "As mentioned previously our new algorithm should inherit from BaseAlgorithm or a subclass of it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### def __init__(self, increment=1):\n",
    "\n",
    "We can override the __init__() method if we need to pass additional configuration information to the algorithm. In this case we pass in the increment which will be used when searching for a feasible rate."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### schedule(self, active_evs)\n",
    "\n",
    "We next need to override the schedule() method. The signature of this method should remain the same, as it is called internally in Simulator. If an algorithm needs additional parameters consider passing them through the constructor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from acnportal.algorithms import BaseAlgorithm\n",
    "\n",
    "class EarliestDeadlineFirstAlgo(BaseAlgorithm):\n",
    "    \"\"\" Algorithm which assigns charging rates to each EV in order of\n",
    "    estimated departure time.\n",
    "\n",
    "    Implements abstract class BaseAlgorithm.\n",
    "\n",
    "    For this algorithm EVs will first be sorted by estimated departure time. We will\n",
    "    then allocate as much\n",
    "    current as possible to each EV in order until the EV is finished charging or an infrastructure\n",
    "    limit is met.\n",
    "\n",
    "    Args:\n",
    "        increment (number): Minimum increment of charging rate. Default: 1.\n",
    "    \"\"\"\n",
    "    def __init__(self, increment=1):\n",
    "        super().__init__()\n",
    "        self._increment = increment\n",
    "        self.max_recompute = 1\n",
    "\n",
    "    def schedule(self, active_evs):\n",
    "        schedule = {ev.station_id: [0] for ev in active_evs}\n",
    "\n",
    "        # Next, we sort the active_evs by their estimated departure time.\n",
    "        sorted_evs = sorted(active_evs, key=lambda x: x.estimated_departure)\n",
    "\n",
    "        # We now iterate over the sorted list of EVs.\n",
    "        for ev in sorted_evs:\n",
    "            # First try to charge the EV at its maximum rate. Remember that each schedule value\n",
    "            #   must be a list, even if it only has one element.\n",
    "            schedule[ev.station_id] = [self.interface.max_pilot_signal(ev.station_id)]\n",
    "\n",
    "            # If this is not feasible, we will reduce the rate.\n",
    "            #   interface.is_feasible() is one way to interact with the constraint set\n",
    "            #   of the network. We will explore another more direct method in lesson 3.\n",
    "            while not self.interface.is_feasible(schedule, 0):\n",
    "\n",
    "                # Since the maximum rate was not feasible, we should try a lower rate.\n",
    "                schedule[ev.station_id][0] -= self._increment\n",
    "\n",
    "                # EVs should never charge below 0 (i.e. discharge) so we will clip the value at 0.\n",
    "                if schedule[ev.station_id][0] < 0:\n",
    "                    schedule[ev.station_id] = [0]\n",
    "                    break\n",
    "        return schedule"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note the structure of the schedule dict which is returned should be something like:\n",
    "\n",
    "```\n",
    "{\n",
    "    'CA-301': [32, 32, 32, 16, 16, ..., 8],\n",
    "    'CA-302': [8, 13, 13, 15, 6, ..., 0],\n",
    "    ...,\n",
    "    'CA-408': [24, 24, 24, 24, 0, ..., 0]\n",
    "}\n",
    "```\n",
    "For the special case when an algorithm only calculates a target rate for the next time interval instead of an entire schedule of rates, the structure should be:\n",
    "\n",
    "```\n",
    "{\n",
    "    'CA-301': [32],\n",
    "    'CA-302': [8],\n",
    "    ...,\n",
    "    'CA-408': [24]\n",
    "}\n",
    "```\n",
    "\n",
    "Note that these are single element lists and NOT floats or integers."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Running the Algorithm\n",
    "\n",
    "Now that we have implemented our algorithm, we can try it out using the same experiment setup as in lesson 1. The only difference will be which scheduling algorithm we use. For fun, lets compare our algorithm against to included implementation of the earliest deadline first algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime\n",
    "import pytz\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.dates as mdates\n",
    "from copy import deepcopy\n",
    "\n",
    "from acnportal import algorithms\n",
    "from acnportal import acnsim\n",
    "\n",
    "\n",
    "# -- Experiment Parameters ---------------------------------------------------------------------------------------------\n",
    "timezone = pytz.timezone('America/Los_Angeles')\n",
    "start = timezone.localize(datetime(2018, 9, 5))\n",
    "end = timezone.localize(datetime(2018, 9, 6))\n",
    "period = 5  # minute\n",
    "voltage = 220  # volts\n",
    "default_battery_power = 32 * voltage / 1000 # kW\n",
    "site = 'caltech'\n",
    "\n",
    "# -- Network -----------------------------------------------------------------------------------------------------------\n",
    "cn = acnsim.sites.caltech_acn(basic_evse=True, voltage=voltage)\n",
    "\n",
    "# -- Events ------------------------------------------------------------------------------------------------------------\n",
    "API_KEY = 'DEMO_TOKEN'\n",
    "events = acnsim.acndata_events.generate_events(API_KEY, site, start, end, period, voltage, default_battery_power)\n",
    "\n",
    "\n",
    "# -- Scheduling Algorithm ----------------------------------------------------------------------------------------------\n",
    "sch = EarliestDeadlineFirstAlgo(increment=1)\n",
    "sch2 = algorithms.SortedSchedulingAlgo(algorithms.least_laxity_first)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# -- Simulator ---------------------------------------------------------------------------------------------------------\n",
    "sim = acnsim.Simulator(deepcopy(cn), sch, deepcopy(events), start, period=period, verbose=False)\n",
    "sim.run()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# For comparison we will also run the builtin earliest deadline first algorithm\n",
    "sim2 = acnsim.Simulator(deepcopy(cn), sch2, deepcopy(events), start, period=period, verbose=False)\n",
    "sim2.run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Results\n",
    "\n",
    "We can now compare the two algorithms side by side by looking that the plots of aggregated current. We see from these plots that our implementation matches th included one quite well. If we look closely however, we might see a small difference. This is because the included algorithm uses a more efficient bisection based method instead of our simpler linear search to find a feasible rate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get list of datetimes over which the simulations were run.\n",
    "sim_dates = mdates.date2num(acnsim.datetimes_array(sim))\n",
    "sim2_dates = mdates.date2num(acnsim.datetimes_array(sim2))\n",
    "\n",
    "# Set locator and formatter for datetimes on x-axis.\n",
    "locator = mdates.AutoDateLocator(maxticks=6)\n",
    "formatter = mdates.ConciseDateFormatter(locator)\n",
    "\n",
    "fig, axs = plt.subplots(1, 2, sharey=True, sharex=True)\n",
    "axs[0].plot(sim_dates, acnsim.aggregate_current(sim), label='Our EDF')\n",
    "axs[1].plot(sim2_dates, acnsim.aggregate_current(sim2), label='Included EDF')\n",
    "axs[0].set_title('Our EDF')\n",
    "axs[1].set_title('Included EDF')\n",
    "for ax in axs:\n",
    "    ax.set_ylabel('Current (A)')\n",
    "    for label in ax.get_xticklabels():\n",
    "        label.set_rotation(40)\n",
    "    ax.xaxis.set_major_locator(locator)\n",
    "    ax.xaxis.set_major_formatter(formatter)\n",
    "\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}